package org.zhaolei.service.impl;

import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.zhaolei.component.StoreEngine;
import org.zhaolei.config.MinioConfig;
import org.zhaolei.controller.req.*;
import org.zhaolei.dto.AccountFileDTO;
import org.zhaolei.dto.FolderTreeNodeDTO;
import org.zhaolei.enums.BizCodeEnum;
import org.zhaolei.enums.FileTypeEnum;
import org.zhaolei.exception.BizException;
import org.zhaolei.mapper.AccountFileMapper;
import org.zhaolei.mapper.FileMapper;
import org.zhaolei.mapper.StorageMapper;
import org.zhaolei.model.AccountFileDO;
import org.zhaolei.model.FileDO;
import org.zhaolei.model.StorageDO;
import org.zhaolei.service.AccountFileService;
import org.zhaolei.enums.FolderFlagEnum;
import org.zhaolei.util.CommonUtil;
import org.zhaolei.util.SpringBeanUtil;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
@Slf4j
public class AccountFileServiceImpl implements AccountFileService {

    @Autowired
    private AccountFileMapper accountFileMapper;

    @Autowired
    private StorageMapper storageMapper;

    @Autowired
    private StoreEngine fileStoreEngine;

    @Autowired
    private MinioConfig minioConfig;

    @Autowired
    private FileMapper fileMapper;

    @Override
    public List<AccountFileDTO> listFile(Long accountId, Long parentId) {
        List<AccountFileDO> accountFileDOList = accountFileMapper.selectList(new QueryWrapper<AccountFileDO>()
                .eq("account_id", accountId)
                .eq("parent_id", parentId)
                .orderByDesc("is_dir")
                .orderByDesc("gmt_create")
        );

        return SpringBeanUtil.copyProperties(accountFileDOList, AccountFileDTO.class);
    }

    @Override
    public Long createFolder(FolderCreateReq req) {
        AccountFileDTO accountFileDTO = AccountFileDTO.builder()
                .accountId(req.getAccountId())
                .parentId(req.getParentId())
                .fileName(req.getFolderName())
                .isDir(FolderFlagEnum.YES.getCode())
                .build();
        return saveAccountFile(accountFileDTO);
    }

    /**
     * 重命名文件
     * 1、检查ID是否存在
     * 2、新旧文件名称不能一样
     * 3、同层文件名称不能一样
     * @param req
     */
    @Override
    public void renameFile(FileUpdateReq req) {
        //检查当前文件是否存在
        AccountFileDO accountFileDO = accountFileMapper.selectOne(new QueryWrapper<AccountFileDO>()
                .eq("id", req.getFileId())
                .eq("account_id", req.getAccountId())
        );
        if(accountFileDO == null) {
            log.error("文件不存在，{}", req);
            throw new BizException(BizCodeEnum.FILE_NOT_EXISTS);
        } else {
            //当前文件存在
            if(Objects.equals(req.getNewFilename(), accountFileDO.getFileName())) {
                log.error("文件名称重复,{}",req);
                throw new BizException(BizCodeEnum.FILE_RENAME_REPEAT);
            }
            //同层文件名不能一样
            Long selectCount = accountFileMapper.selectCount(new QueryWrapper<AccountFileDO>()
                    .eq("account_id", req.getAccountId())  //确保是当前用户的文件或文件夹
                    .eq("parent_id", accountFileDO.getParentId())  //确保在同一层级，即父文件id相同
                    .eq("file_name", req.getNewFilename()));
            if(selectCount > 0) {
                log.error("文件名称重复,{}",req);
                throw new BizException(BizCodeEnum.FILE_RENAME_REPEAT);
            } else {
                accountFileDO.setFileName(req.getNewFilename());
                accountFileMapper.updateById(accountFileDO);
            }
        }
    }

    @Override
    public List<FolderTreeNodeDTO> folderTree(Long accountId) {
        //查询用户的全部文件夹
        List<AccountFileDO> accountFileDOList = accountFileMapper.selectList(new QueryWrapper<AccountFileDO>()
                .eq("account_id", accountId)
                .eq("is_dir", FolderFlagEnum.YES.getCode()));

        if(CollectionUtils.isEmpty(accountFileDOList)) {
            return List.of();
        }

        //构建一个map，key是文件夹id，value是文件夹对象，相当于一个数据源
        Map<Long, FolderTreeNodeDTO> folderMap = accountFileDOList.stream()
                .collect(Collectors.toMap(AccountFileDO::getId, accountFileDO -> FolderTreeNodeDTO.builder()
                        .id(accountFileDO.getId())
                        .parentId(accountFileDO.getParentId())
                        .label(accountFileDO.getFileName())
                        .children(new ArrayList<>())
                        .build()
                ));

        //构建文件树，遍历数据源，为每个文件夹找到子文件夹
        for(FolderTreeNodeDTO node : folderMap.values()) {
            Long parentId = node.getParentId();

            if(parentId != null && folderMap.containsKey(parentId)) {
                //获取父文件
                FolderTreeNodeDTO parentNode = folderMap.get(parentId);
//获取父文件夹的子节点位置
                List<FolderTreeNodeDTO> children = parentNode.getChildren();
                //将当前节点添加到对应的文件夹里面
                children.add(node);
            }
        }

        //过滤根节点，即parentID为0的，跟节点在用户视角时不存在的，且根节点的children包含了自己，不准确
        List<FolderTreeNodeDTO> rootFolderList = folderMap.values().stream()
                .filter(node -> Objects.equals(node.getParentId(), 0L))
                .collect(Collectors.toList());
        return rootFolderList;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void fileUpload(FileUploadReq req) {
        boolean storageEnough = checkAndUpdateCapacity(req.getAccountId(), req.getFileSize());
        if(storageEnough) {
            //上传到存储引擎
            String storeFileObjectKey = storeFile(req);
            //保存文件关系+保存账号和文件的关系
            saveFileAndAccountFile(req, storeFileObjectKey);
        } else {
            throw new BizException(BizCodeEnum.FILE_STORAGE_NOT_ENOUGH);
        }
    }

    @Override
    public void moveBatch(FileBatchReq req) {
        //检查被移动的文件是否合法
        List<AccountFileDO> accountFileDOList = checkFileIdLegal(req.getFileIds(), req.getAccountId());
        //检查目标文件夹是否合法，包括子文件夹
        checkTargetParentIdLegal(req);

        //批量转移文件到目标文件夹，这里批量更改了parent id，但是更新到数据库里了嘛？？？？
        accountFileDOList.forEach(accountFileDO -> accountFileDO.setParentId(req.getTargetParentId()));
        //到这一步移动完可能会有重复的文件名
        accountFileDOList.forEach(accountFileDO -> {
            Long selectCount = processFileNameDuplicate(accountFileDO);
            //到这一步把重复的文件名都进行重命名了，但是新文件还没存到数据库里。
            if(selectCount > 0) {
                //把文件存到数据库里！这里的逻辑还有点不太清晰，要配合测试搞清楚！！！！！
                accountFileMapper.updateById(accountFileDO);
            }
        });
    }

    /**
     * 文件的批量删除
     * 步骤一：检查是否满足：1、文件ID数量是否合法，2、文件是否属于当前用户
     * 步骤二：判断文件是否是文件夹，文件夹的话需要递归获取里面子文件ID，然后进行批量删除
     * 步骤三：需要更新账号存储空间使用情况
     * 步骤四：批量删除账号映射文件，考虑回收站如何设计
     * @param req
     */
    @Override
    public void delBatch(FileDelReq req) {
        //步骤一：检查是否满足：1、文件ID数量是否合法，2、文件是否属于当前用户
        List<AccountFileDO> accountFileDOList = checkFileIdLegal(req.getFileIds(), req.getAccountId());

        //步骤二：判断文件是否是文件夹，文件夹的话需要递归获取里面子文件ID，然后进行批量删除
        List<AccountFileDO> storeAccountFileDOList = new ArrayList<>();
        //把所有子文件夹和子文件都存到storeAccountFileDOList里面
        findAllAccountFileDOWithRecur(storeAccountFileDOList, accountFileDOList, false);
        //拿到全部文件的id列表
        List<Long> allFileIdList = storeAccountFileDOList.stream()
                .map(AccountFileDO::getId)
                .collect(Collectors.toList());

        //步骤三：需要更新账号存储空间使用情况 可以加个分布式锁，redission 作业，提示可以用account_id锁粒度
        long allFileSize = storeAccountFileDOList.stream()
                .filter(accountFileDO -> Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.NO.getCode()))
                .mapToLong(AccountFileDO::getFileSize)
                .sum();
        //只需要计算改用户id下总的存储空间释放了多少就行
        StorageDO storageDO = storageMapper.selectOne(new QueryWrapper<StorageDO>()
                .eq("account_id", req.getAccountId()));
        storageDO.setUsedSize(storageDO.getUsedSize() - allFileSize);
        storageMapper.updateById(storageDO);

        // 步骤四：批量删除账号映射文件，考虑回收站如何设计
        accountFileMapper.deleteBatchIds(allFileIdList);
    }

    /**
     * * 检查被转移的文件ID是否合法
     * * 检查目标文件夹ID是否合法
     * * 执行拷贝，递归查找【差异点，ID是全新的】
     * * 计算存储空间大小，检查是否足够【差异点，空间需要检查】
     * * 存储相关记录
     * @param req
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void copyBatch(FileBatchReq req) {
        //步骤一：检查是否满足：1、文件ID数量是否合法，2、文件是否属于当前用户
        List<AccountFileDO> accountFileDOList = checkFileIdLegal(req.getFileIds(), req.getAccountId());

        //检查目标文件夹id是否合法
        checkTargetParentIdLegal(req);

        //执行拷贝，递归查找【差异点，ID是全新的】
        List<AccountFileDO> newAccountFileDOList = findBatchCopyFileWithRecur(accountFileDOList, req.getTargetParentId());

        //计算存储空间大小，检查是否足够【差异点，空间需要检查】
        long totalFileSize = newAccountFileDOList.stream()
                //筛选出非文件夹的文件，用于计算存储空间大小
                .filter(file -> Objects.equals(file.getIsDir(), FolderFlagEnum.NO.getCode()))
                .mapToLong(AccountFileDO::getFileSize)
                .sum();
        //这里返回false的话，说明空间超了，就抛出异常
        if(!checkAndUpdateCapacity(req.getAccountId(), totalFileSize)) {
            throw new BizException(BizCodeEnum.FILE_STORAGE_NOT_ENOUGH);
        }

        //存储，批量复制文件（夹）的本质就是批量往AccountFile表中插入新数据
        accountFileMapper.insertFileBatch(newAccountFileDOList);
    }

    /**
     * 文件秒传
     * 1、检查文件是否存在
     * 2、检查空间是否足够
     * 3、建立关系
     * @param req
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean secondUpload(FileSecondUploadReq req) {
        //检查文件是否存在
        FileDO fileDO = fileMapper.selectOne(new QueryWrapper<FileDO>().eq("identifier", req.getIdentifier()));

        //检查空间是否足够
        if(fileDO != null && checkAndUpdateCapacity(req.getAccountId(), fileDO.getFileSize())) {
            //处理文件秒传，其实就是保存一个账号文件关系
            AccountFileDTO accountFileDTO = new AccountFileDTO();

//            accountFileDTO.setAccountId(req.getAccountId())
//                    .setFileId(fileDO.getId())
//                    .setFileName(fileDO.getFileName())
//                    .setFileSize(fileDO.getFileSize())
//                    .setDel(false)
//                    .setIsDir(FolderFlagEnum.NO.getCode());
            accountFileDTO.setAccountId(req.getAccountId());
            accountFileDTO.setFileId(fileDO.getId());
            accountFileDTO.setParentId(req.getParentId());
            accountFileDTO.setFileName(req.getFilename());
            accountFileDTO.setFileSize(fileDO.getFileSize());
            accountFileDTO.setDel(false);
            accountFileDTO.setIsDir(FolderFlagEnum.NO.getCode());

            //保存文件账户关联关系，会检查父文件是否正常，以及名称是否有重复
            saveAccountFile(accountFileDTO);
            return true;
        }
        return false;
    }

    /**
     * 包括递归处理，生成新的ID
     * @param accountFileDOList
     * @param targetParentId
     * @return
     */
    public List<AccountFileDO> findBatchCopyFileWithRecur(List<AccountFileDO> accountFileDOList, Long targetParentId) {
        List<AccountFileDO> newAccountFileDOList = new ArrayList<>();
        //对accountFileDOList进行遍历，对每一个对象都生成一个新的对象保存到newAccountFileDOList中，一直遍历到叶子结点
        accountFileDOList.forEach(accountFileDO -> doCopyChildRecord(newAccountFileDOList, accountFileDO, targetParentId));

        return newAccountFileDOList;
    }

    /**
     * 递归处理，包括子文件夹
     * @param newAccountFileDOList
     * @param accountFileDO
     * @param targetParentId
     */
    private void doCopyChildRecord(List<AccountFileDO> newAccountFileDOList, AccountFileDO accountFileDO, Long targetParentId) {
        //保存旧的ID，方便查找子文件（夹）
        Long oldAccountFileId = accountFileDO.getId();
        //创建新记录
        accountFileDO.setId(IdUtil.getSnowflakeNextId());
        accountFileDO.setParentId(targetParentId);
        accountFileDO.setGmtModified(null);
        accountFileDO.setGmtCreate(null);

        //处理重复的文件（夹），如果重复就会重命名
        processFileNameDuplicate(accountFileDO);

        //纳入容器存储
        newAccountFileDOList.add(accountFileDO);

        //判断是文件还是文件夹，递归处理
        if(Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.YES.getCode())) {
            //获取子文件（夹）列表
            List<AccountFileDO> childAccountFileDOList = findChildAccountFile(accountFileDO.getAccountId(), oldAccountFileId);
            if(CollectionUtils.isEmpty(childAccountFileDOList)) {
                return;
            }
            childAccountFileDOList.forEach(childAccountFileDO -> {
                //递归处理
                doCopyChildRecord(newAccountFileDOList, childAccountFileDO, accountFileDO.getId());
            });
        }
    }

    /**
     * 查找当前parentId下的子文件（夹），不递归
     * @param accountId
     * @param parentId
     * @return
     */
    private List<AccountFileDO> findChildAccountFile(Long accountId, Long parentId) {
        return accountFileMapper.selectList(new QueryWrapper<AccountFileDO>()
                .eq("account_id", accountId)
                .eq("parent_id", parentId));
    }

    /**
     * 检查目标文件夹ID是否合法,包括子文件夹
     * 1、目标的文件ID不能是文件
     * 2、要操作的文件列表不能包括目标文件ID
     * @param req
     */
    private void checkTargetParentIdLegal(FileBatchReq req) {
        //目标的文件id不能是文件，必须是文件夹
        AccountFileDO targetAccountFileDO = accountFileMapper.selectOne(new QueryWrapper<AccountFileDO>()
                .eq("id", req.getTargetParentId())
                .eq("is_dir", FolderFlagEnum.YES.getCode())
                .eq("account_id", req.getAccountId()));
        if(targetAccountFileDO == null) {
            log.error("目标文件ID不是文件，需要是文件夹，targetParentId={}", req.getTargetParentId());
            throw new BizException(BizCodeEnum.FILE_TARGET_PARENT_ILLEGAL);
        }

        /**
         * 要操作的文件列表不能包括目标文件ID
         * 思路
         * 1、查询批量操作的文件和文件夹，递归处理
         * 2、判断是否在里面
         */
        List<AccountFileDO> prepareAccountFileDOList = accountFileMapper.selectList(new QueryWrapper<AccountFileDO>()
                .in("id", req.getFileIds())
                .eq("account_id", req.getAccountId()));

        //定义一个容器，存储全部文件夹，包括子文件夹
        List<AccountFileDO> allAccountFileDOList = new ArrayList<>();
        //递归查找全部子文件夹
        findAllAccountFileDOWithRecur(allAccountFileDOList, prepareAccountFileDOList, false);

        //判断目标文件夹是否在批量操作的文件夹里面                                      //当前DO的id等于targetparent的id
        if(allAccountFileDOList.stream().anyMatch(accountFileDO1 -> Objects.equals(accountFileDO1.getId(), req.getTargetParentId()))) {
            log.error("目标文件夹在批量移动的目标里，targetParentId={}", req.getTargetParentId());
            throw new BizException(BizCodeEnum.FILE_TARGET_PARENT_ILLEGAL);
        }
    }

    /**
     * 递归查找prepare中的全部子文件夹，并且存储到allAccountFileDOList中
     * @param allAccountFileDOList
     * @param prepareAccountFileDOList
     * @param onlyFolder
     */
    public void findAllAccountFileDOWithRecur(List<AccountFileDO> allAccountFileDOList,
                                              List<AccountFileDO> prepareAccountFileDOList, boolean onlyFolder) {
        for(AccountFileDO accountFileDO : prepareAccountFileDOList) {
            if(Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.YES.getCode())) {
                //递归查找子文件夹
                List<AccountFileDO> children = accountFileMapper.selectList(new QueryWrapper<AccountFileDO>()
                        .eq("parent_id", accountFileDO.getId()));
                findAllAccountFileDOWithRecur(allAccountFileDOList, children, onlyFolder);
            }
            //如果通过onlyFolder是true,只存储文件夹到allAccountFileDOList，否则都存储到allAccountFileDOList，注意一下这个逻辑！！
            if(!onlyFolder || Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.YES.getCode())) {
                allAccountFileDOList.add(accountFileDO);
            }
        }
    }

    /**
     * 检查被移动的文件id是否合法，思路是从数据库中查找文件id，看是否都能找到
     * @param fileIds
     * @param accountId
     * @return
     */
    public List<AccountFileDO> checkFileIdLegal(List<Long> fileIds, Long accountId) {
        List<AccountFileDO> accountFileDOList = accountFileMapper
                .selectList(new QueryWrapper<AccountFileDO>().in("id", fileIds).eq("account_id", accountId));

        if(accountFileDOList.size() != fileIds.size()) {
            log.error("文件ID数量不合法,ids={}", fileIds);
            throw new BizException(BizCodeEnum.FILE_BATCH_UPDATE_ERROR);
        }
        //进一步完善的话，可以加个set防止重复元素

        return accountFileDOList;
    }

    /**
     * 梳理一下这个函数里的不同方法之间的不同点和逻辑关系！！！！
     * @param req
     * @param storeFileObjectKey
     */
    @Override
    public void saveFileAndAccountFile(FileUploadReq req, String storeFileObjectKey) {
        //保存文件
        FileDO fileDO = saveFile(req, storeFileObjectKey);

        //保存文件账号关系
        AccountFileDTO accountFileDTO = AccountFileDTO.builder()
                .accountId(req.getAccountId())
                .parentId(req.getParentId())
                .fileId(fileDO.getId())
                .fileName(req.getFilename())
                .isDir(FolderFlagEnum.NO.getCode())
                .fileSuffix(fileDO.getFileSuffix())
                .fileSize(req.getFileSize())
                .fileType(FileTypeEnum.fromExtension(fileDO.getFileSuffix()).name())
                .build();
        //把账号文件关系存储到Accountfile表中
        saveAccountFile(accountFileDTO);
    }

    /**
     * 简单搜索接口
     * @param accountId
     * @param search
     * @return
     */
    @Override
    public List<AccountFileDTO> search(Long accountId, String search) {
        List<AccountFileDO> accountFileDOList = accountFileMapper.selectList(new QueryWrapper<AccountFileDO>()
                .like("file_name", search)
                .eq("account_id", accountId)
                .orderByDesc("is_dir")
                .orderByDesc("gmt_create").last("limit 30"));

        return SpringBeanUtil.copyProperties(accountFileDOList, AccountFileDTO.class);
    }

    /**
     * 把file对象存到file表中
     * @param req
     * @param storeFileObjectKey
     * @return
     */
    private FileDO saveFile(FileUploadReq req, String storeFileObjectKey) {
        FileDO fileDO = new FileDO();
        fileDO.setAccountId(req.getAccountId());
        fileDO.setFileName(req.getFilename());
        fileDO.setFileSize(req.getFile() != null ? req.getFile().getSize() : req.getFileSize());
        fileDO.setFileSuffix(CommonUtil.getFileSuffix(req.getFilename()));
        fileDO.setObjectKey(storeFileObjectKey);
        fileDO.setIdentifier(req.getIdentifier());
        fileMapper.insert(fileDO);
        return fileDO;
    }

    /**
     * 上传到存储引擎，并返回存储的文件路径
     * @param req
     * @return
     */
    private String storeFile(FileUploadReq req) {
        String objectKey = CommonUtil.getFilePath(req.getFilename());
        fileStoreEngine.upload(minioConfig.getBucketName(), objectKey, req.getFile());
        return objectKey;
    }

    /**
     * 检查存储空间和更新存储空间，返回false说明空间超了
     * @param accountId
     * @param fileSize
     * @return
     */
    private boolean checkAndUpdateCapacity(Long accountId, Long fileSize) {
        StorageDO storageDO = storageMapper.selectOne(new QueryWrapper<StorageDO>().eq("account_id", accountId));

        Long totalSize = storageDO.getTotalSize();
        if(storageDO.getUsedSize() + fileSize <= totalSize) {
            storageDO.setUsedSize(storageDO.getUsedSize() + fileSize);
            storageMapper.updateById(storageDO);
            return true;
        } else {
            return false;
        }
    }

    private Long saveAccountFile(AccountFileDTO accountFileDTO) {
        //检查父文件是否存在，如果存在父文件且异常则会报错
        checkParentFileId(accountFileDTO);

        AccountFileDO accountFileDO = SpringBeanUtil.copyProperties(accountFileDTO, AccountFileDO.class);
        //检查文件是否重复 aa  aa(1) aa(2)
        processFileNameDuplicate(accountFileDO);
        //保存相关文件关系
        accountFileMapper.insert(accountFileDO);

        return accountFileDO.getId();
    }

    /**
     * 处理文件是否重复,
     * 文件夹重复和文件名重复处理规则不一样
     * @param accountFileDO
     * @return
     */
    public Long processFileNameDuplicate(AccountFileDO accountFileDO) {
        Long selectCount = accountFileMapper.selectCount(new QueryWrapper<AccountFileDO>()
                .eq("account_id", accountFileDO.getAccountId())
                .eq("parent_id", accountFileDO.getParentId())
                .eq("is_dir", accountFileDO.getIsDir())
                .eq("file_name", accountFileDO.getFileName()));
        //这个函数的作用是在文件存入数据库之前判断重复，所以selectCount大于0就说明有重复的
        if(selectCount>0){
            //处理重复文件夹，大于0说明存在一个同名的文件夹或者文件
            if(Objects.equals(accountFileDO.getIsDir(), FolderFlagEnum.YES.getCode())){
                //如果文件夹同名则把文件名改成文件夹名+时间戳
                accountFileDO.setFileName(accountFileDO.getFileName()+"_"+System.currentTimeMillis());
            }else {
                //处理重复文件名,提取文件拓展名
                String[] split = accountFileDO.getFileName().split("\\.");
                //给文件名添加时间戳
                accountFileDO.setFileName(split[0]+"_"+System.currentTimeMillis()+"."+split[1]);
            }
        }

        return selectCount;
    }

    /**
     * 检查父文件是否存在
     * @param accountFileDTO
     */
    private void checkParentFileId(AccountFileDTO accountFileDTO) {
        //parent id为0说明是root文件夹
        if(accountFileDTO.getParentId() != 0) {
            AccountFileDO accountFileDO = accountFileMapper.selectOne(
                    new QueryWrapper<AccountFileDO>()
                            .eq("id", accountFileDTO.getParentId())
                            .eq("account_id", accountFileDTO.getAccountId())
            );
            if (accountFileDO == null) {
                throw new BizException(BizCodeEnum.FILE_NOT_EXISTS);
            }
        }
    }
}
